

import { EventEmitter } from '@angular/core';

import LayerImage from 'ol/layer/image';
import LayerVector from 'ol/layer/vector';
import LayerTile from 'ol/layer/tile';
import SourceImageWMS from 'ol/source/imagewms';
import SourceTileWMS from 'ol/source/tilewms';
import SourceVector from 'ol/source/vector';
import FormatGeoJSON from 'ol/format/geojson';
import Feature from 'ol/feature';
import GeomPoint from 'ol/geom/point';
import FormatFilterWithin from 'ol/format/filter/within';
import FormatFilterAnd from 'ol/format/filter/and';
import FormatFilterOr from 'ol/format/filter/or';
import FormatFilterEqualTo from 'ol/format/filter/equalto';
import FormatFilterNotEqualTo from 'ol/format/filter/notequalto';
import FormatFilterIsLike from 'ol/format/filter/islike';
import FormatFilterGreaterThan from 'ol/format/filter/greaterthan';
import FormatFilterGreaterThanOrEqualTo from 'ol/format/filter/greaterthanorequalto';
import FormatFilterLessThan from 'ol/format/filter/lessthan';
import FormatFilterLessThanOrEqualTo from 'ol/format/filter/lessthanorequalto';
import FormatFilterIsNull from 'ol/format/filter/isnull';
import FormatFilterNot from 'ol/format/filter/not';
import proj from 'ol/proj';

import * as SLDReader from '../../utils/SLDReader';

import { DefaultStyle } from './style.model';
import { Layer, SimStyle } from '../class';
import { LayerTypes } from '../enums';
import { LayersService } from '../services';
import { QueryParams } from './query-params.model';
import { WFSOptions, JSONOptions, JSONXYOptions, FeaturePropertyType } from './layer.model';
import { QueryRuleResult, QueryRule, OperatorTypes } from '../components/properties-query';
import { Observable, Observer } from 'rxjs';
import { map, catchError, switchMap } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';

export class CustomLayer extends Layer {

    private loading$: EventEmitter<any> = new EventEmitter();
    private loaded$: EventEmitter<{ [key: string]: any }> = new EventEmitter();
    private error$: EventEmitter<any> = new EventEmitter();
    private query$: EventEmitter<string[]> = new EventEmitter();
    private fieldTypes$: EventEmitter<FeaturePropertyType[]> = new EventEmitter();

    private colorIndex: number = Math.floor(Math.random()*(11-0+1)+0);
    private layer: any;
    private addedToMap: boolean = false;
    private defaultStyle: DefaultStyle = new DefaultStyle(this.colorIndex);
    private highlightStyle: DefaultStyle = new DefaultStyle(this.colorIndex, true);

    private spaceQueryCondition: any;
    private queryCondition: any;

    private queryParams: QueryParams;
    private queryURL: string;
    private queryRules: QueryRuleResult = null;
    private features: any[] = [];
    private extent: number[];


    constructor(
        private map: any,
        private layersService: LayersService,
        options: Layer
    ) {
        super();
        this.bindParams(options);
        if(this.mapViewable) {
            this.layer = this.buildLayer();
            if(this.type === LayerTypes.JSONXY || this.type === LayerTypes.WFS || this.type === LayerTypes.GEOJSON) {
                this.setLayerStyle();
            }
            this.addToMap();
        } else {
            this.loadLayer();
        }
        if(this.type === LayerTypes.WFS) {
            this.queryParams = this.queryParamsBuilder(<WFSOptions>this.options);
            this.queryURL = this.queryURLBuilder(<WFSOptions>this.options);
        }
        if(this.extentOptions) {
            this.extent = proj.transformExtent(this.extentOptions.extent, this.extentOptions.srs || 'EPSG:3857', 'EPSG:3857');
        }
    }

    addFeatures(features: any[]): CustomLayer {
        const source = this.getSource();
        (features && source) && source.addFeatures(features);
        return this;
    }

    clearFeatures(): CustomLayer {
        const source = this.getSource();
        source && source.clear();
        return this;
    }

    remove(): CustomLayer {
        this.removeFromMap();
        return this;
    }

    setOpacity(opacity: number): CustomLayer {
        this.opacity = opacity;
        if(this.layer) {
            this.layer.setOpacity(opacity);
        }
        return this;
    }

    setVisible(visible: boolean): CustomLayer {
        this.visiable = visible;
        if(this.layer) {
            this.layer.setVisible(visible);
        }
        return this;
    }

    setZIndex(zIndex: number): CustomLayer {
        this.zIndex = zIndex;
        if(this.layer) {
            this.layer.setZIndex(zIndex);
        }
        return this;
    }

    getColorIndex(): number {
        return this.colorIndex;
    }

    updateQueryResult(result): CustomLayer {
        this.queryResult = result;
        if(this.type === LayerTypes.JSONXY || this.type === LayerTypes.WFS) {
            this.updateLayerFeatures();
        }
        return this;
    }

    getSource(): any {
        if(this.layer) {
            return this.layer.getSource();
        } else {
            return null;
        }
    }

    getFeatureById(id: string): any {
        const source = this.getSource();
        if(source) {
            return source.getFeatureById(id);
        } else {
            return null;
        }
    }

    getLayer(): any {
        return this.layer;
    }

    getLayerId(): string {
        return this.id;
    }

    spaceQuery(features: any[]): CustomLayer {
        if(this.type === LayerTypes.WFS && this.queryable) {
            if(features && features.length > 0) {
                const { geometryName, srsName } = <WFSOptions>this.options;
                const conditions = features.map(_feature => new FormatFilterWithin(geometryName, _feature.getGeometry(), srsName));
                if(conditions.length > 1) {
                    this.spaceQueryCondition = new FormatFilterOr(...conditions);
                } else {
                    this.spaceQueryCondition = conditions[0];
                }
            } else {
                this.spaceQueryCondition = null;
            }
            this.query();
        }
        return this;
    }

    propertiesQuery(queryRules: QueryRuleResult, callback?: () => any): CustomLayer {
        this.queryRules = queryRules;
        this.queryCondition = queryRules ? this.serializeQueryRules(queryRules) : null;
        this.query(callback);
        return this;
    }

    on(eventType: 'loading'|'loaded'|'loadError'|'query'|'fieldTypes'): EventEmitter<any|{ [key: string]: any }> {
        switch(eventType) {
            case 'loading': {
                return this.loading$;
            }
            case 'loaded': {
                return this.loaded$;
            }
            case 'loadError': {
                return this.error$;
            }
            case 'query': {
                return this.query$;
            }
            case 'fieldTypes': {
                return this.fieldTypes$;
            }
            default: {
                return new EventEmitter();
            }
        }
    }

    reload(): CustomLayer {
        this.loadLayer().subscribe(
            data => this.setLoaded(data), 
            error => this.setLoadError(error)
        );
        return this;
    }

    getQueryRules(): QueryRuleResult {
        return this.queryRules;
    }

    removePropertyQuery(): CustomLayer {
        this.propertiesQuery(null);
        return this;
    }

    removeSpaceQuery(): CustomLayer {
        this.spaceQuery(null);
        return this;
    }

    removeQuery(): CustomLayer {
        if(this.type === LayerTypes.WFS) {
            this.queryResult = null;
            this.queryCondition = null,
            this.queryRules = null;
            this.spaceQueryCondition = null;
            this.updateLayerFeatures();
        }
        return this;
    }

    getExtent(): number[] {
        return this.extent;
    }

    zoomToLayerExtent(): void {
        const extent = this.getExtent();
        const view = this.map.getView();
        if(extent) {
            view.fit(extent, {
                padding: [20, 20, 20, 20],
                duration: 500
            });
        }
    }

    private bindParams(params: Layer): void {
        Object.keys(params).map((key: string) => this[key] = params[key]);
    }

    private buildLayer(): any {
        switch(this.type) {
            case LayerTypes.JSONXY: {
                return this.buildLayerJSONXY();
            }
            case LayerTypes.WFS: {
                return this.buildLayerWFS();
            }
            case LayerTypes.WMS: {
                return this.buildLayerWMS();
            }
            case LayerTypes.GEOJSON: {
                return this.buildLayerByGeoJSON();
            }
        }
    }

    private buildLayerJSONXY(): any {
        const configure = this.options;
        let layer = new LayerVector({
            renderMode: 'vector',
            source: new SourceVector(),
            opacity: this.opacity || 1,
            visible: this.visiable,
            zIndex: this.zIndex
        });
        return layer;
    }

    private buildLayerWMS(): any {
        const configure = this.options;
        const source = new SourceTileWMS({
            ...configure
        });
        const layer = new LayerTile({
            source,
            opacity: this.opacity || 1,
            zIndex: this.zIndex,
            visible: this.visiable
        });
        return layer;
    }

    private buildLayerWFS(): any {
        const configure = { ...this.options, url: undefined };
        let layer = new LayerVector({
            renderMode: 'vector',
            source: new SourceVector({
                ...configure,
                format: new FormatGeoJSON()
            }),
            opacity: this.opacity || 1,
            visible: this.visiable,
            zIndex: this.zIndex
        });
        return layer;
    }

    private buildLayerByGeoJSON(): any {
        const format = new FormatGeoJSON();
        const features = (this.geojson ? format.readFeatures(this.geojson) : []).map((_feature, _index) => {
            if(!_feature.getId()) {
                _feature.setId(_index);
            }
            _feature.set('LAYER_ID', this.id);
            return _feature;
        });
        const layer = new LayerVector({
            renderMode: 'vector',
            source: new SourceVector({ features }),
            opacity: this.opacity || 1,
            visible: this.visiable,
            zIndex: this.zIndex
        });
        this.features = features;
        return layer;
    }

    private switchStyle(style: SimStyle, type: string): any {
        if(type === 'Point' || type === 'MultiPoint') {
            return style.getPointAsOl();
        } else if(type === 'LineString' || type === 'MultiLineString') {
            return style.getLineAsOl();
        } else if(type === 'Polygon' || type === 'MultiPolygon') {
            return style.getPolygonAsOl();
        } else {
            return style.getStyleAsOl();
        }
    }

    private setLayerStyle(): any {
        if(this.style) {
            const style = new SimStyle(this.style);
            this.layer.setStyle(feature => {
                const selected: boolean = feature.get('selected');
                const geometry = feature.getGeometry();
                const type = geometry.getType();
                if(selected) {
                    if(this.styleActive) {
                        const styleActive = new SimStyle(this.styleActive);
                        return this.switchStyle(styleActive, type);
                    } else {
                        return this.highlightStyle.style(geometry.getType());
                    }
                } else {
                    return this.switchStyle(style, type);
                }
            });
        } else if(this.geoserverStyle) {
            this.layersService.loadSource(this.geoserverStyle.url, { responseType: 'text' })
                .subscribe(data => {
                    const sldObject = SLDReader.Reader(data);
                    const sldLayer = SLDReader.getLayer(sldObject, this.geoserverStyle.layerName);
                    if(sldLayer) {
                        const sldStyle = SLDReader.getStyle(sldLayer, this.geoserverStyle.styleName);
                        const format = new FormatGeoJSON();
                        this.layer.setStyle((feature, resolution) => {
                            const selected: boolean = feature.get('selected');
                            if(selected) {
                                const geometry = feature.getGeometry();
                                return this.highlightStyle.style(geometry.getType());
                            } else {
                                const geojson = JSON.parse(format.writeFeature(feature));
                                const rules = SLDReader.getRules(
                                    sldStyle.featuretypestyles['0'],
                                    geojson,
                                    resolution * 111034
                                );
                                return SLDReader.OlStyler(SLDReader.getGeometryStyles(rules), geojson.geometry.type);                             
                            }
                        });
                    }
                });
        } else {
            this.layer.setStyle(feature => {
                if(!feature) return null;
                const selected: boolean = feature.get('selected');
                let geometry = feature.getGeometry();
                let style = this.defaultStyle.style(geometry.getType());
                if(selected) {
                    style = this.highlightStyle.style(geometry.getType());
                }
                return style;
            });
        }
    }

    private addToMap(): void {
        if(this.layer) {
            this.map.addLayer(this.layer);
        }
        this.loadLayer().subscribe(
            data => {
                this.setLoaded(data)
                if(this.zoomTo) {
                    this.zoomToLayerExtent();
                    this.zoomTo = false;
                }
            }, 
            error => this.setLoadError(error)
        );
        this.addedToMap = true;
    }

    private removeFromMap(): void {
        if(this.layer) {
            this.map.removeLayer(this.layer);
        }
    }

    private loadLayer(): Observable<any> {
        this.setLoading();
        switch(this.type) {
            case LayerTypes.JSON: {
                const options = <JSONOptions>this.options;
                return this.layersService.loadSource(options.url).pipe(
                    map((data: any) => {
                        return data.map((value, index) => {
                            value['FEATURE_ID'] = value[this.idFieldName];
                            return value;
                        });
                    }),
                    catchError(error => of(error))
                );
            }
            case LayerTypes.JSONXY: {
                const configure: JSONXYOptions = <JSONXYOptions>this.options;
                const x: string = configure.x;
                const y: string = configure.y;
                const projection: 'EPSG:4326'|'EPSG:3857' = configure.projection || 'EPSG:3857';
                return this.layersService.loadSource(configure.url).pipe(
                    map((data: any) => {
                        let source = this.layer.getSource();
                        const features: any[] = data.map((item: any, index) => {
                            let geomPoint = new GeomPoint([item[x], item[y]]);
                            geomPoint.transform(projection, 'EPSG:3857');
                            let feature = new Feature({
                                geometry: geomPoint
                            });
                            item['FEATURE_ID'] = item[this.idFieldName];
                            feature.setId(item['FEATURE_ID']);
                            feature.set('LAYER_ID', this.id);
                            feature.setProperties(item);
                            return feature;
                        });
                        this.features = features;
                        this.updateLayerFeatures();
                        return data;
                    }),
                    catchError(error => of(error))
                );
            }
            case LayerTypes.WFS: {
                const options: WFSOptions = <WFSOptions>this.options;
                const url: string = `${options.host}/geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${options.namespace}:${options.name}&srsname=${options.srsName}&outputFormat=application%2Fjson`;
                return this.layersService.loadSource(url).pipe(
                    switchMap((result: any) => {
                        const features = (new FormatGeoJSON()).readFeatures(result)
                            .map(feature => {
                                feature.set('LAYER_ID', this.id);
                                return feature;
                            });
                        const data = features.map(_feautre => this.filterProperties(_feautre));
                        this.features = features;
                        return this.layersService.describeFeatureType(<WFSOptions>this.options).pipe(
                            map((fieldTypes: FeaturePropertyType[]) => {
                                if(this.alias) {
                                    fieldTypes = fieldTypes.map(fieldType => {
                                        const alias = this.alias[fieldType.name] || fieldType.alias;
                                        return { ...fieldType, alias };
                                    });
                                }
                                this.fieldTypes = fieldTypes;
                                this.fieldTypes$.emit(fieldTypes);
                                this.updateLayerFeatures();
                                return data;
                            }),
                            catchError(error => of(error))
                        );
                    }),
                    catchError(error => of(error))
                );
            }
            case LayerTypes.WMS: {
                let source = this.layer.getSource();
                source.on('tileloadstart', () => this.setLoading());
                return Observable.create((observer: Observer<any>) => {
                    source.on('tileloadend', () => {
                        observer.next(null);
                    });
                    source.on('tileloaderror', (error) => {
                        observer.error(error);
                    });
                });
            }
            case LayerTypes.GEOJSON: {
                const data = this.features.map(_feautre => this.filterProperties(_feautre));
                return of(data);
            }
            default: {
                return of(null);
            }
        }
    }

    private queryParamsBuilder(options: WFSOptions): QueryParams {
        const queryParams: QueryParams = {
            featureNS: options.namespaceURI,
            featurePrefix: options.namespace,
            featureTypes: [options.name],
            srsName: options.srsName,
            outputFormat: 'application/json'
        };
        return queryParams;
    }

    private queryURLBuilder(options: WFSOptions): string {
        return `${options.host}/geoserver/${options.namespace}/ows`;
    }

    private query(callback?: () => void): void {
        let filter = this.formatFilterCondition();
        if(this.type === LayerTypes.WFS && this.queryParams && this.queryable && filter) {
            const queryParams: QueryParams = { ...this.queryParams, filter };
            this.setLoading();
            this.layersService.query(this.queryURL, queryParams).subscribe(result => {
                let queryResult = [];
                if(result.features) {
                    queryResult = result.features.map(_feature => this.filterProperties(this.features.find(feature => feature.getId() === _feature.id)));
                }
                this.setLoaded(this.data);
                this.query$.emit(queryResult);
                callback && callback();
            }, error => this.setLoadError(error));
        } else {
            this.query$.emit(null);
            callback && callback();
        }
    }

    private formatFilterCondition(): any {
        let filter = null;
        if(this.spaceQueryCondition && this.queryCondition) {
            filter = new FormatFilterAnd(this.spaceQueryCondition, this.queryCondition);
        }
        if(this.spaceQueryCondition && !this.queryCondition) {
            filter = this.spaceQueryCondition;
        }
        if(!this.spaceQueryCondition && this.queryCondition) {
            filter = this.queryCondition;
        }
        return filter;
    }

    private updateLayerFeatures(): void {
        const queryResult = this.queryResult;
        let features = this.features;
        if(queryResult) {
            const ids = this.queryResult.map(_data => _data['FEATURE_ID']);
            features = features.filter(_feature => ids.includes(_feature.getId()));
        }
        this.clearFeatures();
        this.addFeatures(features);
    }

    private layerExtentBuilder(): void {
        if(!this.extent && this.type !== LayerTypes.JSON && this.type !== LayerTypes.WMS) {
            this.extent = this.getSource().getExtent();
        }
    }

    private setLoading(): void {
        this.loading = true;
        this.loaded = false;
        this.loadError = null;
        this.disabled = true;
        setTimeout(() => this.loading$.emit(), 0);
    }

    private setLoaded(data: { [key: string]: any }): void {
        this.loading = false;
        this.loaded = true;
        this.loadError = null;
        this.disabled = false;
        this.layerExtentBuilder();
        setTimeout(() => this.loaded$.emit(data), 0);
    }

    private setLoadError(error): void {
        this.loading = false;
        this.loaded = false;
        this.loadError = error;
        this.disabled = false;
        
        setTimeout(() => this.error$.emit(error), 0);
    }

    private filterProperties(
        feature: any, 
        removed: string[] = ['bbox', 'geometry', 'LAYER_ID', 'selected']
    ): { [key: string]: any } {
        const data = feature.getProperties();
        return Object.keys(data).reduce((_data, _key) => {
            if(!removed.includes(_key)) {
                _data = { ..._data, [_key]: data[_key] };
            }
            return _data;
        }, { 
            'FEATURE_ID': feature.getId()
        });
    }

    private serializeQueryRules(queryRules: QueryRuleResult): any {
        const rules: QueryRule[] = queryRules.rules;
        const conditions = rules.map((rule: QueryRule) => {
            switch(rule.operator) {
                case OperatorTypes.equal: {
                    return new FormatFilterEqualTo(rule.field, rule.value, false);
                }
                case OperatorTypes.unequal: {
                    return new FormatFilterNotEqualTo(rule.field, rule.value, false);
                }
                case OperatorTypes.contain: {
                    return new FormatFilterIsLike(rule.field, `*${rule.value}*`);
                }
                case OperatorTypes.greaterThan: {
                    return new FormatFilterGreaterThan(rule.field, rule.value);
                }
                case OperatorTypes.greaterThanOrEqual: {
                    return new FormatFilterGreaterThanOrEqualTo(rule.field, rule.value);
                }
                case OperatorTypes.lessThan: {
                    return new FormatFilterLessThan(rule.field, rule.value);
                }
                case OperatorTypes.lessThanOrEqual: {
                    return new FormatFilterLessThanOrEqualTo(rule.field, rule.value);
                }
                case OperatorTypes.empty: {
                    return new FormatFilterIsNull(rule.field);
                }
                case OperatorTypes.unEmpty: {
                    return new FormatFilterNot(new FormatFilterIsNull(rule.field));
                }
            }
        });
        if(conditions.length > 1) {
            if(queryRules.operator === 'AND') {
                return new FormatFilterAnd(...conditions);
            } else if(queryRules.operator === 'OR') {   
                return new FormatFilterOr(...conditions);
            }
        } else {
            return conditions[0];
        }
        
    }

}